    using System;
using System.Collections.Generic;
using System.Text;
using Noein.Math;
using Noein.Physics;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace Noein.Control
{
    /// <summary>
    /// Linear-motion-only 3D physics simulation.
    /// </summary>
    public class ParticleController : Controller
    {
        #region Fields

        //private float fMin = 0.0001f;
        //private float muS = 1f;



        private Integrate Integrate = NumericalIntegration.RungeKutta4;
        public Integrate Integrator
        {
            get { return Integrate; }
            set { Integrate = value; }
        }

        private LinkedList<Force> forces = new LinkedList<Force>();

        #endregion

        public void AddForce(Force f)
        {
            // HACK friction need to be applied last
            if (f as Friction != null)
            {
                forces.AddLast(f);
            }
            else
            {
                forces.AddFirst(f);
            }
        }

        public override void AttachTo(Object obj)
        {
            if (obj.Spatial == null) obj.Spatial = new Spatial();
            if (obj.Physical == null) obj.Physical = new Physical(obj.Spatial);

            obj.Controllers.Add(this);
        }

        public override void Update(Object obj, float dt)
        {
            if (Pause)
                return;

            // the target this controller modifies
            Physical target = obj.Physical;

            // compute current position by integrating previous velocity over elapsed time
            if (target.Velocity != Vector3.Empty)
            {
                target.Position = new Vector3(
                    Integrate(target.Position.X, target.Velocity.X, dt),
                    Integrate(target.Position.Y, target.Velocity.Y, dt),
                    Integrate(target.Position.Z, target.Velocity.Z, dt));
            }

            // compute current momentum by integrating previous force over elapsed time
            if (target.Force != Vector3.Empty)
            {
                target.Momentum = new Vector3(
                    Integrate(target.Momentum.X, target.Force.X, dt),
                    Integrate(target.Momentum.Y, target.Force.Y, dt),
                    Integrate(target.Momentum.Z, target.Force.Z, dt));
            }

            // TODO should this use current or previous physics states
            // compute current forces from current physics states
            Vector3 totalForce = target.CustomForce;
            foreach (Force force in forces)
            {
                totalForce += force.Compute(obj);
            }
            target.Force = totalForce;
        }
    }

    // TODO
    // public class 2DPhysicsController : Controller

    // TODO
    // public class TranslationalPhysicsController : Controller

    // TODO
    //public class RidigBodyController : TranlationalPhysicsController

    // TODO
    public class ParticlesController : Controller
    {
        private float lifeTime;
        public float LifeTime
        {
            get { return lifeTime; }
            set { lifeTime = value; updateCapacity = true; }
        }

        private float rate;
        public float Rate
        {
            get { return rate; }
            set { rate = value; updateCapacity = true; }
        }

        bool updateCapacity = false;

        private float spreadAngle;
        public float SpreadAngle
        {
            get { return spreadAngle; }
            set { spreadAngle = value; }
        }
        
        private float spreadRadius;
        public float SpreadRadius
        {
            get { return spreadRadius; }
            set { spreadRadius = value; }
        }

        private float randomness;
        //private float liveCount;
        private Physical physicalSetting;

        private Controller controller;
        private Random rand;

        private int capacity;

        public ParticlesController(
            float lifeTime, float rate, float spreadAngle, float spreadRadius, 
            float randomness, Physical physicalSetting) // forces
        {
            this.lifeTime = lifeTime;
            this.rate = rate;
            this.spreadAngle = spreadAngle * (float)System.Math.PI / 180f;
            this.spreadRadius = spreadRadius;
            this.randomness = randomness;
            this.physicalSetting = physicalSetting;

            // TODO
            // switch(physicalSetting.gettype) determine physical controller
            controller = new ParticleController();
            ((ParticleController)controller).Integrator = Math.NumericalIntegration.Euler;
            rand = new Random();
        }

        public override void Reset(Object obj)
        {
            base.Reset(obj);

            ParticleList target = obj as ParticleList;
            foreach (ParticleList.Particle particle in target.Particles)
            {
                //particle.Spatial = new Spatial();
                //particle.Physical =
                //    new Physical(physicalSetting.Mass, particle.Spatial, physicalSetting.Velocity, physicalSetting.Acceleration);
                
                // make particle invisible
                particle.LifeTime = 0;
                particle.Age = 0;
            }

            target.ActiveCount = 0;
            target.TimeSinceLastSpawn = 0;
        }

        public override void Update(Object obj, float dt)
        {
            if (Pause)
                return;

            ParticleList target = obj as ParticleList;

            target.TimeSinceLastSpawn += dt;

            if (updateCapacity)
            {
                RecomputeCapacity();
                if (capacity != target.Particles.Count)
                    ReallocatePool(target);
            }

            LinkedListNode<ParticleList.Particle> node = target.Particles.First;
            for (int i = 0; i < target.ActiveCount; i++)
            {
                // update particle age
                node.Value.Age += dt;
                if (node.Value.Age > node.Value.LifeTime)
                {
                    // remove dead particle
                    target.ActiveCount--;
                    LinkedListNode<ParticleList.Particle> removeNode = node;
                    node = node.Next;
                    target.Particles.Remove(removeNode);
                    target.Particles.AddLast(removeNode);
                }
                else
                {
                    node = node.Next;
                }
            }

            // spawn
            int numToSpawn = (int)(target.TimeSinceLastSpawn * rate);
            if (numToSpawn > 0) target.TimeSinceLastSpawn -= numToSpawn / rate;
            for (int i = 0; i < numToSpawn; i++)
            {
                node.Value.LifeTime = lifeTime - lifeTime * (float)rand.NextDouble() * randomness;
                node.Value.Age = 0;

                // random pick initial spawn position, within the spread radius
                Vector2 position = new Vector2();
                float radiusSq = 0;
                do
                {
                    position.X = (float)rand.NextDouble() * spreadRadius * 2 - spreadRadius;
                    position.Y = (float)rand.NextDouble() * spreadRadius * 2 - spreadRadius;
                } while ((radiusSq = position.LengthSq()) > spreadRadius * spreadRadius);
                node.Value.Spatial.TeleportTo(
                    Vector3.TransformCoordinate(new Vector3(position.X, 0, position.Y), obj.Spatial.Transform));

                // TODO simplify
                // derive ejection vector
                float a = position.Length();
                float angle = spreadAngle * a / spreadRadius;
                
                Vector3 direction;
                if (angle != 0)
                {
                    float b = a / (float)System.Math.Tan(angle);
                    direction = new Vector3(position.X, 0, position.Y) + new Vector3(0, 1, 0) * b;
                    direction.Normalize();
                }
                else
                {
                    direction = new Vector3(0, 1, 0);
                }
                direction.TransformCoordinate(obj.Spatial.Transform);

                //Vector3 v1 = node.Value.Spatial.Position;
                //v1.Normalize();
                //Vector3 v2 = new Vector3(0, 1, 0);
                //v1 = v1 * (float)(System.Math.Tan(angle));
                //Vector3 direction = v1 + v2;
                //direction.Normalize();

                node.Value.Spatial.Orientation = Quaternion.Identity;
                node.Value.Physical.Momentum =
                    direction * node.Value.Physical.Mass * physicalSetting.Velocity.Length();
                    //* direction * ,
                    //obj.Spatial.OrientationMatrix);

                //node.Value.Physical.
                //node.Value.Physical = 
                //    new Physical(physicalSetting.Mass, node.Value.Spatial, physicalSetting.Velocity, physicalSetting.Acceleration);

                target.ActiveCount++;
                node = node.Next;
                if (node == null) 
                    throw new Exception("trying to spawn more particle than particle pool has!");
            }
        }

        public override void AttachTo(Object obj)
        {
            ParticleList target = obj as ParticleList;
            if (target != null)
            {
                // validates the object
                if (obj.Spatial == null) obj.Spatial = new Spatial();
                if (obj.Visual == null) throw new Exception("Particle list need visual!");

                target.Controllers.Add(this);

                RecomputeCapacity();
                ReallocatePool(target);

                // reset particles
                Reset(obj);
            }
        }

        private void RecomputeCapacity()
        {
            // allocate particle pool
            capacity = (int)(rate * (lifeTime + 2));
        }

        private void ReallocatePool(ParticleList target)
        {
            while (target.Particles.Count < capacity)
            {
                target.Particles.AddLast(
                    new LinkedListNode<ParticleList.Particle>(new ParticleList.Particle()));
                target.Particles.Last.Value.Controllers.Add(controller);
                target.Particles.Last.Value.Spatial = new Spatial();
                target.Particles.Last.Value.Physical = new Physical(target.Particles.Last.Value.Spatial);
                target.Particles.Last.Value.Physical.Mass = physicalSetting.Mass;
            }
            
            while (target.Particles.Count > capacity)
            {
                target.Particles.RemoveLast();
            }

            if (target.ActiveCount > capacity)
                target.ActiveCount = capacity;

            target.TimeSinceLastSpawn = 0;
        }
    }
}
